'use strict';

import Vue from 'vue'
import { dom as $ } from './dom'

const _ = Vue.util

const rAF = window.requestAnimationFrame  ||
    window.webkitRequestAnimationFrame  ||
    window.mozRequestAnimationFrame     ||
    window.oRequestAnimationFrame       ||
    window.msRequestAnimationFrame      ||
    function (callback) { window.setTimeout(callback, 1000 / 60); };


/*
 * 工具类
 */
const utils = (function () {

    let me = {};

    let _elementStyle = document.createElement('div').style;

    let _vendor = (function () {
        let vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'],
            transform,
            i = 0,
            l = vendors.length;

        for (;i < l; i++) {
            transform = vendors[i] + 'ransform';
            if ( transform in _elementStyle ) return vendors[i].substr(0, vendors[i].length-1);
        }

        return false;
    })();

    function _prefixStyle (style) {
        if ( _vendor === false ) return false;
        if ( _vendor === '' ) return style;
        return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
    }


    me.getTime = Date.now || function getTime () { return new Date().getTime(); };

    me.prefixPointerEvent = function (pointerEvent) {
        return window.MSPointerEvent ?
            'MSPointer' + pointerEvent.charAt(9).toUpperCase() + pointerEvent.substr(10):
            pointerEvent;
    };


    /**
     * 根据一定时间内的滑动距离计算出最终停止距离和时间。
     * @param current：当前滑动位置
     * @param start：touchStart 时候记录的开始位置，但是在touchmove时候可能被重写
     * @param time：touchstart 到手指离开时候经历的时间，同样可能被touchmove重写
     * @param lowerMargin：可移动的最大距离，这个一般为计算得出 this.wrapperHeight - this.scrollerHeight
     * @param wrapperSize：如果有边界距离的话就是可拖动，不然碰到0的时候便停止
     * @param deceleration：匀减速
     * @returns {{destination: number, duration: number}}
     */
    me.momentum = function (current, start, time, lowerMargin, wrapperSize, deceleration) {
        let distance = current - start,
            speed = Math.abs(distance) / time,
            destination,
            duration;

        deceleration = deceleration === undefined ? 0.0006 : deceleration;

        destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 );
        duration = speed / deceleration;

        if ( destination < lowerMargin ) {
            destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin;
            distance = Math.abs(destination - current);
            duration = distance / speed;
        } else if ( destination > 0 ) {
            destination = wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0;
            distance = Math.abs(current) + destination;
            duration = distance / speed;
        }

        return {
            destination: Math.round(destination),
            duration: duration
        };
    };

    let _transform = _prefixStyle('transform');

    _.extend(me, {
        hasTransform: _transform !== false,
        hasPerspective: _prefixStyle('perspective') in _elementStyle,
        hasTouch: 'ontouchstart' in window,
        hasPointer: window.PointerEvent || window.MSPointerEvent, // IE10 is prefixed
        hasTransition: _prefixStyle('transition') in _elementStyle
    });

    // This should find all Android browsers lower than build 535.19 (both stock browser and webview)
    me.isBadAndroid = /Android /.test(window.navigator.appVersion) && !(/Chrome\/\d/.test(window.navigator.appVersion));

    _.extend(me.style = {}, {
        transform: _transform,
        transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
        transitionDuration: _prefixStyle('transitionDuration'),
        transitionDelay: _prefixStyle('transitionDelay'),
        transformOrigin: _prefixStyle('transformOrigin'),
        transitionProperty: _prefixStyle('transitionProperty')
    });


    me.offset = function (el) {
        var left = -el.offsetLeft,
            top = -el.offsetTop;

        while (el = el.offsetParent) {
            left -= el.offsetLeft;
            top -= el.offsetTop;
        }
        return {
            left: left,
            top: top
        };
    };


    /*
     * 配合 config 里面的 preventDefaultException 属性
     * 不对匹配到的 element 使用 e.preventDefault()
     * 默认阻止所有事件的冒泡，包括 click 或 tap
     */
    me.preventDefaultException = function (el, exceptions) {
        for ( var i in exceptions ) {
            if ( exceptions[i].test(el[i]) ) {
                return true;
            }
        }
        return false;
    };


    _.extend(me.eventType = {}, {
        touchstart: 1,
        touchmove: 1,
        touchend: 1,

        mousedown: 2,
        mousemove: 2,
        mouseup: 2,

        pointerdown: 3,
        pointermove: 3,
        pointerup: 3,

        MSPointerDown: 3,
        MSPointerMove: 3,
        MSPointerUp: 3
    });


    _.extend(me.ease = {}, {
        quadratic: {
            style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
            fn: function (k) {
                return k * ( 2 - k );
            }
        },
        circular: {
            style: 'cubic-bezier(0.1, 0.57, 0.1, 1)',   // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1)
            fn: function (k) {
                return Math.sqrt( 1 - ( --k * k ) );
            }
        },
        back: {
            style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
            fn: function (k) {
                var b = 4;
                return ( k = k - 1 ) * k * ( ( b + 1 ) * k + b ) + 1;
            }
        },
        bounce: {
            style: '',
            fn: function (k) {
                if ( ( k /= 1 ) < ( 1 / 2.75 ) ) {
                    return 7.5625 * k * k;
                } else if ( k < ( 2 / 2.75 ) ) {
                    return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75;
                } else if ( k < ( 2.5 / 2.75 ) ) {
                    return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375;
                } else {
                    return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375;
                }
            }
        },
        elastic: {
            style: '',
            fn: function (k) {
                var f = 0.22,
                    e = 0.4;

                if ( k === 0 ) { return 0; }
                if ( k == 1 ) { return 1; }

                return ( e * Math.pow( 2, - 10 * k ) * Math.sin( ( k - f / 4 ) * ( 2 * Math.PI ) / f ) + 1 );
            }
        }
    });

    me.tap = function (e, eventName) {
        var ev = document.createEvent('Event');
        ev.initEvent(eventName, true, true);
        ev.pageX = e.pageX;
        ev.pageY = e.pageY;
        e.target.dispatchEvent(ev);
    };

    me.click = function (e) {
        var target = e.target,
            ev;
        if ( !(/(SELECT|INPUT|TEXTAREA)/i).test(target.tagName) ) {
            ev = document.createEvent('MouseEvents');
            ev.initMouseEvent('click', true, true, e.view, 1,
                target.screenX, target.screenY, target.clientX, target.clientY,
                e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
                0, null);

            ev._constructed = true;
            target.dispatchEvent(ev);
        }
    };

    return me;
})();


/*
 * 构造函数
 */
export default class Scroll {
    constructor(el, options) {
        this.wrapper = typeof el == 'string' ? $(el)[0] : el;

        this.options = {
            startX: 0,                  // 初始化 X 坐标
            startY: 0,                  // 初始化 Y 坐标
            scrollY: true,              // 竖向滚动
            scrollX: false,             // 默认非水平
            directionLockThreshold: 5,  // 确定滚动方向的阈值
            momentum: true,             // 是否开启惯性滚动

            duration: 300,              // transition 过渡时间

            bounce: true,               // 是否有反弹动画
            bounceTime: 600,            // 反弹动画时间
            bounceEasing: '',           // 反弹动画类型：'circular'(default), 'quadratic', 'back', 'bounce', 'elastic'

            preventDefault: true,       // 是否阻止默认滚动事件（和冒泡有区别）
            eventPassthrough: true,     // 穿透，是否触发原生滑动（取值 true、false、vertical、horizental）

            freeScroll: false,          // 任意方向的滚动。若 scrollX 和 scrollY 同时开启，则相当于 freeScroll

            bindToWrapper : true,       // 事件是否绑定到 wrapper 元素上，否则大部分绑定到 window（若存在嵌套，则绑定在元素上最好）
            resizePolling : 60,         // resize 时候隔 60ms 就执行 refresh 方法重新获取位置信息(事件节流)

            disableMouse : false,       // 是否禁用鼠标
            disableTouch : false,       // 是否禁用touch事件
            disablePointer : false,     // 是否禁用win系统的pointer事件

            tap: true,                  // 是否模拟 tap 事件
            click: false,               // 是否模拟点击事件（false 则使用原生click事件）

            preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/ }, // 当遇到正则内的元素则不阻止冒泡

            HWCompositing: true,        // Hardware acceleration
            useTransition: true,        // Transition || requestAnimationFrame
            useTransform: true          // Translate || Left/Top
        };

        for (let i in options) {
            this.options[i] = options[i];
        }

        // scroller
        // ==================================
        if (!this.options.role && this.options.scrollX === false) {
            this.options.eventPassthrough = 'horizontal';   // 竖直滚动的 scroller 不拦截横向原生滚动
        }

        // slide
        // ==================================
        if (this.options.role === 'slider') {

            this.options.scrollX = true;
            this.options.scrollY = false;
            this.options.momentum = false;

            this.scroller = $('.ui-slider-content').$el;
            $(this.scroller.children[0]).addClass('current');

            this.currentPage = 0;
            this.count = this.scroller.children.length;

            this.scroller.style.width = this.count+"00%";

            this.itemWidth = this.scroller.children[0].clientWidth;
            this.scrollWidth = this.itemWidth * this.count;

            if (this.options.indicator) {
                var temp = '<ul class="ui-slider-indicators">';

                for (var i=1; i<=this.count; i++) {
                    if (i===1) {
                        temp += '<li class="current">'+i+'</li>';
                    }
                    else {
                        temp += '<li>'+i+'</li>';
                    }
                }
                temp += '</ul>';
                $(this.wrapper).append(temp);
                this.indicator = $('.ui-slider-indicators').$el;
            }
        }

        // tab
        // ==================================
        else if(this.options.role === 'tab') {
            this.options.scrollX = true;
            this.options.scrollY = false;
            this.options.momentum = false;

            this.scroller = $('.ui-tab-content')[0];
            this.nav = $('.ui-tab-nav')[0];

            $(this.scroller.children[0]).addClass('current');
            $(this.nav.children[0]).addClass('current');

            this.currentPage = 0;
            this.count = this.scroller.children.length;

            this.scroller.style.width = this.count+"00%";

            this.itemWidth = this.scroller.children[0].clientWidth;
            this.scrollWidth = this.itemWidth * this.count;
        } else {
            this.scroller = this.wrapper.children[0];
        }

        this.scrollerStyle = this.scroller.style;

        this.translateZ = utils.hasPerspective && this.options.HWCompositing ? ' translateZ(0)' : '';
        this.options.useTransition = utils.hasTransition && this.options.useTransition;
        this.options.useTransform = utils.hasTransform && this.options.useTransform;
        this.options.eventPassthrough = this.options.eventPassthrough === true ? 'vertical' : this.options.eventPassthrough;
        this.options.preventDefault = !this.options.eventPassthrough && this.options.preventDefault;
        // If you want eventPassthrough I have to lock one of the axes
        this.options.scrollX = this.options.eventPassthrough == 'horizontal' ? false : this.options.scrollX;
        this.options.scrollY = this.options.eventPassthrough == 'vertical' ? false : this.options.scrollY;
        // With eventPassthrough we also need lockDirection mechanism
        this.options.freeScroll = this.options.freeScroll && !this.options.eventPassthrough;
        this.options.directionLockThreshold = this.options.eventPassthrough ? 0 : this.options.directionLockThreshold;
        this.options.bounceEasing = typeof this.options.bounceEasing == 'string' ?
            utils.ease[this.options.bounceEasing] || utils.ease.circular :
            this.options.bounceEasing;
        this.options.resizePolling = this.options.resizePolling === undefined ? 60 : this.options.resizePolling;

        if (this.options.tap === true) {
            this.options.tap = 'tap';
        }

        if (this.options.useTransform === false) {
            this.scroller.style.position = 'relative';
        }

        // Some defaults
        this.x = 0;
        this.y = 0;
        this.directionX = 0;
        this.directionY = 0;
        this._events = {};

        this._initEvents();   // 绑定各种事件
        this.refresh();

        this.scrollTo(this.options.startX, this.options.startY);
        this.enable();

        // 自动播放
        if (this.options.autoplay) {
            let context = this;
            this.options.interval = this.options.interval || 2000;
            this.options.flag = setTimeout(function(){
                context._autoplay.apply(context)
            }, context.options.interval);
        }
    }

    _initEvents(remove) {
        let eventType = remove ?
            function(el, type, fn) {
                $(el).off(type, fn);
            } : function(el, type, fn) {
                $(el).on(type, fn);
            };

        let target = this.options.bindToWrapper ? this.wrapper : window;

        /*
         * 给 addEventListener 传递 this
         * 程序会自动找到 handleEvent 方法作为回调函数
         */
        eventType(window, 'orientationchange', this);
        eventType(window, 'resize', this);

        if (this.options.click) {
            eventType(this.wrapper, 'click', this, true);
        }

        if (!this.options.disableMouse) {
            eventType(this.wrapper, 'mousedown', this);
            eventType(target, 'mousemove', this);
            eventType(target, 'mousecancel', this);
            eventType(target, 'mouseup', this);
        }

        if (utils.hasPointer && !this.options.disablePointer) {
            eventType(this.wrapper, utils.prefixPointerEvent('pointerdown'), this);
            eventType(target, utils.prefixPointerEvent('pointermove'), this);
            eventType(target, utils.prefixPointerEvent('pointercancel'), this);
            eventType(target, utils.prefixPointerEvent('pointerup'), this);
        }

        if (utils.hasTouch && !this.options.disableTouch) {
            eventType(this.wrapper, 'touchstart', this);
            eventType(target, 'touchmove', this);
            eventType(target, 'touchcancel', this);
            eventType(target, 'touchend', this);
        }

        eventType(this.scroller, 'transitionend', this);
        eventType(this.scroller, 'webkitTransitionEnd', this);
        eventType(this.scroller, 'oTransitionEnd', this);
        eventType(this.scroller, 'MSTransitionEnd', this);

        // tab
        // =============================
        if(this.options.role === 'tab') {
            eventType(this.nav, 'touchend', this);
            eventType(this.nav, 'mouseup', this);
            eventType(this.nav, 'pointerup', this);
        }
    }

    refresh() {
        var rf = this.wrapper.offsetHeight; // Force reflow

        // http://jsfiddle.net/y8Y32/25/
        // clientWidth = content + padding
        this.wrapperWidth   = this.wrapper.clientWidth;
        this.wrapperHeight  = this.wrapper.clientHeight;


        // 添加 wrapper 的 padding 值到 scroller 身上，更符合使用预期
        var matrix = window.getComputedStyle(this.wrapper, null);
        var pt = matrix['padding-top'].replace(/[^-\d.]/g, ''),
            pb = matrix['padding-bottom'].replace(/[^-\d.]/g, ''),
            pl = matrix['padding-left'].replace(/[^-\d.]/g, ''),
            pr = matrix['padding-right'].replace(/[^-\d.]/g, '');

        var matrix2 = window.getComputedStyle(this.scroller, null);
        var mt2 = matrix2['margin-top'].replace(/[^-\d.]/g, ''),
            mb2 = matrix2['margin-bottom'].replace(/[^-\d.]/g, ''),
            ml2 = matrix2['margin-left'].replace(/[^-\d.]/g, ''),
            mr2 = matrix2['margin-right'].replace(/[^-\d.]/g, '');


        // offsetWidth = content + padding + border
        this.scrollerWidth  = this.scroller.offsetWidth+parseInt(pl)+parseInt(pr)+parseInt(ml2)+parseInt(mr2);
        this.scrollerHeight = this.scroller.offsetHeight+parseInt(pt)+parseInt(pb)+parseInt(mt2)+parseInt(mb2);


        // slide
        // ==================================
        if (this.options.role === 'slider' || this.options.role === 'tab') {
            this.itemWidth = this.scroller.children[0].clientWidth;
            this.scrollWidth = this.itemWidth * this.count;
            this.scrollerWidth = this.scrollWidth;
        }

        this.maxScrollX     = this.wrapperWidth - this.scrollerWidth;
        this.maxScrollY     = this.wrapperHeight - this.scrollerHeight;

        this.hasHorizontalScroll    = this.options.scrollX && this.maxScrollX < 0;
        this.hasVerticalScroll      = this.options.scrollY && this.maxScrollY < 0;

        if ( !this.hasHorizontalScroll ) {
            this.maxScrollX = 0;
            this.scrollerWidth = this.wrapperWidth;
        }

        if ( !this.hasVerticalScroll ) {
            this.maxScrollY = 0;
            this.scrollerHeight = this.wrapperHeight;
        }

        this.endTime = 0;
        this.directionX = 0;
        this.directionY = 0;

        this.wrapperOffset = utils.offset(this.wrapper);
        this.resetPosition();
    }

    handleEvent(e) {
        switch ( e.type ) {
            case 'touchstart':
            case 'pointerdown':
            case 'MSPointerDown':
            case 'mousedown':
                this._start(e);
                break;
            case 'touchmove':
            case 'pointermove':
            case 'MSPointerMove':
            case 'mousemove':
                this._move(e);
                break;
            case 'touchend':
            case 'pointerup':
            case 'MSPointerUp':
            case 'mouseup':
            case 'touchcancel':
            case 'pointercancel':
            case 'MSPointerCancel':
            case 'mousecancel':
                this._end(e);
                break;
            case 'orientationchange':
            case 'resize':
                this._resize();
                break;
            case 'transitionend':
            case 'webkitTransitionEnd':
            case 'oTransitionEnd':
            case 'MSTransitionEnd':
                this._transitionEnd(e);
                break;
            case 'wheel':
            case 'DOMMouseScroll':
            case 'mousewheel':
                this._wheel(e);
                break;
            case 'keydown':
                this._key(e);
                break;
            case 'click':
                if ( !e._constructed ) {
                    e.preventDefault();
                    e.stopPropagation();
                }
                break;
        }
    }

    _start(e) {

        if ( utils.eventType[e.type] != 1 ) {   // 如果是鼠标点击，则只响应鼠标左键
            if ( e.button !== 0 ) {
                return;
            }
        }

        if ( !this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated) ) {
            return;
        }

        // 如果 preventDefault === true 且 不是落后的安卓版本 且 不是需要过滤的 target 就阻止默认的行为
        if ( this.options.preventDefault && !utils.isBadAndroid && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
            e.preventDefault();
        }

        var point = e.touches ? e.touches[0] : e,   // 检验是触摸事件对象还是鼠标事件对象
            pos;

        this.initiated  = utils.eventType[e.type];  // 初始化事件类型（1：触摸，2：鼠标，3：pointer）
        this.moved      = false;
        this.distX      = 0;
        this.distY      = 0;
        this.directionX = 0;
        this.directionY = 0;
        this.directionLocked = 0;

        this._transitionTime();
        this.startTime = utils.getTime();

        // 定住正在滑动的 scroller，slider/tab 不这么做
        if ( this.options.useTransition && this.isInTransition && this.options.role !== 'slider' && this.options.role !== 'tab') {
            this.isInTransition = false;
            pos = this.getComputedPosition();
            this._translate(Math.round(pos.x), Math.round(pos.y));
        }
        // 场景：（没有使用 Transition 属性）
        else if ( !this.options.useTransition && this.isAnimating ) {
            this.isAnimating = false;
        }

        this.startX    = this.x;
        this.startY    = this.y;
        this.absStartX = this.x;
        this.absStartY = this.y;
        this.pointX    = point.pageX;
        this.pointY    = point.pageY;

        // throttle
        // ======================
        if (this.options.autoplay) {
            var context = this;

            clearTimeout(this.options.flag);
            this.options.flag = setTimeout(function() {
                context._autoplay.apply(context);
            }, context.options.interval);
        }

        event.stopPropagation();
    }

    _move(e) {

        if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {    // 如果事件类型和 touchstart 初始化的事件类型不一致，退出
            return;
        }
        if ( this.options.preventDefault ) {    // 这么做才能确保 Android 下 touchend 能被正常触发（需测试）
            e.preventDefault();
        }
        var point       = e.touches ? e.touches[0] : e,
            deltaX      = point.pageX - this.pointX,
            deltaY      = point.pageY - this.pointY,
            timestamp   = utils.getTime(),
            newX, newY,
            absDistX, absDistY;

        this.pointX     = point.pageX;
        this.pointY     = point.pageY;

        this.distX      += deltaX;
        this.distY      += deltaY;
        absDistX        = Math.abs(this.distX);
        absDistY        = Math.abs(this.distY);


        // 如果在很长的时间内只移动了少于 10 像素的距离，那么不会触发惯性滚动
        if ( timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10) ) {
            return;
        }

        // 屏蔽滚动方向的另外一个方向（可配置）
        if ( !this.directionLocked && !this.options.freeScroll ) {
            if ( absDistX > absDistY + this.options.directionLockThreshold ) {
                this.directionLocked = 'h';     // lock horizontally
            } else if ( absDistY >= absDistX + this.options.directionLockThreshold ) {
                this.directionLocked = 'v';     // lock vertically
            } else {
                this.directionLocked = 'n';     // no lock
            }
        }
        if ( this.directionLocked == 'h' ) {
            // slider/tab 外层高度自适应
            if (this.options.role === 'tab') {
                $(this.scroller).children('li').height('auto');
            }
            if ( this.options.eventPassthrough == 'vertical' ) {
                e.preventDefault();
            } else if ( this.options.eventPassthrough == 'horizontal' ) {
                this.initiated = false;
                return;
            }
            deltaY = 0; // 不断重置垂直偏移量为 0
        }
        else if ( this.directionLocked == 'v' ) {
            if ( this.options.eventPassthrough == 'horizontal' ) {
                e.preventDefault();
            } else if ( this.options.eventPassthrough == 'vertical' ) {
                this.initiated = false;
                return;
            }
            deltaX = 0; // 不断重置水平偏移量为 0
        }

        deltaX = this.hasHorizontalScroll ? deltaX : 0;
        deltaY = this.hasVerticalScroll ? deltaY : 0;

        newX = this.x + deltaX;
        newY = this.y + deltaY;

        // Slow down if outside of the boundaries
        if ( newX > 0 || newX < this.maxScrollX ) {
            newX = this.options.bounce ? this.x + deltaX / 3 : newX > 0 ? 0 : this.maxScrollX;
        }
        if ( newY > 0 || newY < this.maxScrollY ) {
            newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
        }

        this.directionX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0;
        this.directionY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0;

        this.moved = true;  // 滚动开始
        this._translate(newX, newY);

        if ( timestamp - this.startTime > 300 ) {   // 每 300 毫秒重置一次初始值
            this.startTime = timestamp;
            this.startX = this.x;
            this.startY = this.y;
        }
    }

    _end(e) {

        if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {
            return;
        }

        if ( this.options.preventDefault && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
            e.preventDefault();
        }

        var point = e.changedTouches ? e.changedTouches[0] : e, // 移开屏幕的那个触摸点，只会包含在 changedTouches 列表中，而不会包含在 touches 或 targetTouches 列表中
            momentumX,
            momentumY,
            duration = utils.getTime() - this.startTime,
            newX = Math.round(this.x),
            newY = Math.round(this.y),
            distanceX = Math.abs(newX - this.startX),
            distanceY = Math.abs(newY - this.startY),
            time = 0,
            easing = '';

        this.isInTransition = 0;
        this.initiated = 0;
        this.endTime = utils.getTime();


        if ( this.resetPosition(this.options.bounceTime) ) {    // reset if we are outside of the boundaries
            if (this.options.role === 'tab') {
                $(this.scroller.children[this.currentPage]).siblings('li').height(0);
            }
            return;
        }

        this.scrollTo(newX, newY);  // ensures that the last position is rounded

        if (!this.moved) {  // we scrolled less than 10 pixels
            if (this.options.tap && utils.eventType[e.type] === 1) {
                utils.tap(e, this.options.tap);
            }
            if ( this.options.click) {
                utils.click(e);
            }
        }

        // 300ms 内的滑动要启动惯性滚动
        if ( this.options.momentum && duration < 300 ) {
            momentumX = this.hasHorizontalScroll ? utils.momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options.deceleration) : { destination: newX, duration: 0 };
            momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options.deceleration) : { destination: newY, duration: 0 };
            newX = momentumX.destination;
            newY = momentumY.destination;
            time = Math.max(momentumX.duration, momentumY.duration);
            this.isInTransition = 1;
        }

        if ( newX != this.x || newY != this.y ) {
            // change easing function when scroller goes out of the boundaries
            if ( newX > 0 || newX < this.maxScrollX || newY > 0 || newY < this.maxScrollY ) {
                easing = utils.ease.quadratic;
            }
            this.scrollTo(newX, newY, time, easing);
            return;
        }


        // tab
        // ==========================
        if (this.options.role === 'tab' && $(event.target).closest('ul').hasClass('ui-tab-nav')) {
            $(this.nav).children().removeClass('current');
            $(event.target).addClass('current');
            var tempCurrentPage = this.currentPage;
            this.currentPage = $(event.target).index();

            $(this.scroller).children().height('auto'); // tab 外层高度自适应
            this._execEvent('beforeScrollStart', tempCurrentPage, this.currentPage);
        }



        // slider & tab
        // ==============================
        if (this.options.role === 'slider' || this.options.role === 'tab') {

            if (distanceX < 30) {
                this.scrollTo(-this.itemWidth*this.currentPage, 0, this.options.bounceTime, this.options.bounceEasing);
            }
            else if (newX-this.startX<0) {  // 向前
                this._execEvent('beforeScrollStart', this.currentPage, this.currentPage+1);
                this.scrollTo(-this.itemWidth*++this.currentPage, 0, this.options.bounceTime, this.options.bounceEasing);
            }
            else if (newX-this.startX>=0) { // 向后
                this._execEvent('beforeScrollStart', this.currentPage, this.currentPage-1);
                this.scrollTo(-this.itemWidth*--this.currentPage, 0, this.options.bounceTime, this.options.bounceEasing);
            }

            // tab 外层高度自适应
            if (this.options.role === 'tab') {
                $(this.scroller.children[this.currentPage]).siblings('li').height(0);
            }

            if (this.indicator && distanceX >= 30) {
                $(this.indicator).children().removeClass('current');
                $(this.indicator.children[this.currentPage]).addClass('current');
            }
            else if (this.nav && distanceX >= 30) {
                $(this.nav).children().removeClass('current');
                $(this.nav.children[this.currentPage]).addClass('current');
            }

            $(this.scroller).children().removeClass('current');
            $(this.scroller.children[this.currentPage]).addClass('current');
        }
    }

    _resize() {
        var that = this;
        clearTimeout(this.resizeTimeout);
        this.resizeTimeout = setTimeout(function () {
            that.refresh();
        }, this.options.resizePolling);
    }

    _transitionEnd(e) {
        if ( e.target != this.scroller || !this.isInTransition ) {
            return;
        }
        this._transitionTime();

        if ( !this.resetPosition(this.options.bounceTime) ) {
            this.isInTransition = false;
            this._execEvent('scrollEnd', this.currentPage);
        }
    }

    destroy() {
        this._initEvents(true);     // 去除事件绑定
    }

    resetPosition(time) {
        var x = this.x,
            y = this.y;

        time = time || 0;

        if ( !this.hasHorizontalScroll || this.x > 0 ) {
            x = 0;
        } else if ( this.x < this.maxScrollX ) {
            x = this.maxScrollX;
        }

        if ( !this.hasVerticalScroll || this.y > 0 ) {
            y = 0;
        } else if ( this.y < this.maxScrollY ) {
            y = this.maxScrollY;
        }

        if ( x == this.x && y == this.y ) {
            return false;
        }
        this.scrollTo(x, y, time, this.options.bounceEasing);
        return true;
    }

    disable() {
        this.enabled = false;
    }

    enable() {
        this.enabled = true;
    }

    on(type, fn) {
        if ( !this._events[type] ) {
            this._events[type] = [];
        }
        this._events[type].push(fn);
    }

    off(type, fn) {
        if ( !this._events[type] ) {
            return;
        }

        var index = this._events[type].indexOf(fn);

        if ( index > -1 ) {
            this._events[type].splice(index, 1);
        }
    }

    _execEvent(type) {
        if ( !this._events[type] ) {
            return;
        }
        var i = 0,
            l = this._events[type].length;

        if ( !l ) {
            return;
        }
        for ( ; i < l; i++ ) {
            this._events[type][i].apply(this, [].slice.call(arguments, 1));
        }
    }

    scrollTo(x, y, time, easing) {
        easing = easing || utils.ease.circular;

        this.isInTransition = this.options.useTransition && time > 0;

        if ( !time || (this.options.useTransition && easing.style) ) {

            if (this.options.role === 'slider' || this.options.role === 'tab') {    // 不添加判断会影响 left/top 的过渡
                time = this.options.duration;
                this.scrollerStyle[utils.style.transitionProperty] = utils.style.transform;
            }
            this.scrollerStyle[utils.style.transitionTimingFunction] = easing.style;
            this._transitionTime(time);
            this._translate(x, y);
        } else {
            this._animate(x, y, time, easing.fn);
        }
    }

    scrollToElement(el, time, offsetX, offsetY, easing) {
        el = el.nodeType ? el : this.scroller.querySelector(el);

        if ( !el ) {
            return;
        }
        var pos = utils.offset(el);
        pos.left -= this.wrapperOffset.left;
        pos.top  -= this.wrapperOffset.top;

        // if offsetX/Y are true we center the element to the screen
        // 若 offsetX/Y 都是 true，则会滚动到元素在屏幕中间的位置
        if ( offsetX === true ) {
            offsetX = Math.round(el.offsetWidth / 2 - this.wrapper.offsetWidth / 2);
        }
        if ( offsetY === true ) {
            offsetY = Math.round(el.offsetHeight / 2 - this.wrapper.offsetHeight / 2);
        }
        pos.left -= offsetX || 0;
        pos.top  -= offsetY || 0;
        pos.left = pos.left > 0 ? 0 : pos.left < this.maxScrollX ? this.maxScrollX : pos.left;
        pos.top  = pos.top  > 0 ? 0 : pos.top  < this.maxScrollY ? this.maxScrollY : pos.top;

        time = time === undefined || time === null || time === 'auto' ? Math.max(Math.abs(this.x-pos.left), Math.abs(this.y-pos.top)) : time;

        this.scrollTo(pos.left, pos.top, time, easing);
    }

    _transitionTime(time) {
        time = time || 0;
        this.scrollerStyle[utils.style.transitionDuration] = time + 'ms';

        if ( !time && utils.isBadAndroid ) {
            this.scrollerStyle[utils.style.transitionDuration] = '0.001s';
        }
    }

    _translate(x, y) {
        if ( this.options.useTransform ) {
            this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
        } else {
            x = Math.round(x);
            y = Math.round(y);
            this.scrollerStyle.left = x + 'px';
            this.scrollerStyle.top = y + 'px';
        }
        this.x = x;
        this.y = y;
    }

    getComputedPosition() {
        var matrix = window.getComputedStyle(this.scroller, null),
            x, y;

        if ( this.options.useTransform ) {
            matrix = matrix[utils.style.transform].split(')')[0].split(', ');
            x = +(matrix[12] || matrix[4]);
            y = +(matrix[13] || matrix[5]);
        } else {
            x = +matrix.left.replace(/[^-\d.]/g, '');
            y = +matrix.top.replace(/[^-\d.]/g, '');
        }

        return { x: x, y: y };
    }

    _animate(destX, destY, duration, easingFn) { // 当浏览器不支持 transition 时提供的退化方案 requestAnimationFrame
        var that = this,
            startX = this.x,
            startY = this.y,
            startTime = utils.getTime(),
            destTime = startTime + duration;

        function step () {
            var now = utils.getTime(),
                newX, newY,
                easing;

            if ( now >= destTime ) {
                that.isAnimating = false;
                that._translate(destX, destY);

                if ( !that.resetPosition(that.options.bounceTime) ) {
                    that._execEvent('scrollEnd', this.currentPage);
                }
                return;
            }

            now = ( now - startTime ) / duration;
            easing = easingFn(now);
            newX = ( destX - startX ) * easing + startX;
            newY = ( destY - startY ) * easing + startY;
            that._translate(newX, newY);

            if ( that.isAnimating ) {
                rAF(step);
            }
        }
        this.isAnimating = true;
        step();
    }

    _autoplay() {
        var self = this,
            curPage = self.currentPage;

        self.currentPage = self.currentPage >= self.count-1 ? 0 : ++self.currentPage;
        self._execEvent('beforeScrollStart', curPage, self.currentPage);    // 对于自动播放的 slider/tab，这个时机就是 beforeScrollStart

        // tab 外层高度自适应
        if (this.options.role === 'tab') {
            $(this.scroller).children().height('auto');
            document.body.scrollTop = 0;
        }
        self.scrollTo(-self.itemWidth*self.currentPage, 0, self.options.bounceTime, self.options.bounceEasing);

        if (self.indicator) {
            $(self.indicator).children().removeClass('current');
            $(self.indicator.children[self.currentPage]).addClass('current');
            $(self.scroller).children().removeClass('current');
            $(self.scroller.children[self.currentPage]).addClass('current');
        }
        else if (self.nav) {
            $(self.nav).children().removeClass('current');
            $(self.nav.children[self.currentPage]).addClass('current');
            $(self.scroller).children().removeClass('current');
            $(self.scroller.children[self.currentPage]).addClass('current');
        }

        self.options.flag = setTimeout(function() {
            self._autoplay.apply(self);
        }, self.options.interval);
    }

};
